#! /usr/bin/env python

# -*- coding: UTF-8 -*-
# ==============================================================================
# Filename : SciSerialEdit.py
# Purpose  : 
# Author   : Yohann Li (lyh001686@sina.com)
# Date     : 2020-07-17 16:15
# ==============================================================================
import re
import sys
from collections import deque
import string

from PyQt5.QtCore import pyqtSignal, QTimer, QObject, QEvent, QThread, QRegularExpression, Qt
from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QPalette, QTextCursor, QColor, qRgb, QFont, qRgba, \
    QInputMethodQueryEvent, QKeyEvent, QKeySequence
from PyQt5.QtWidgets import QTextEdit, QScrollBar, QApplication, QPlainTextEdit
from PyQt5.Qsci import QsciScintilla, QsciLexerCustom, QsciLexer
from Widgets.Highlighter import Highlighter
from Widgets.ScrollBar import ScrollBar
from configuration import logger, mode_mark, valid_cmd_pat, valid_cmd_splitter, line_background_color, \
    caret_width, case_sensitivity, color_pat, font_style, caret_line_visible


class TextEdit(QsciScintilla):
    open_pySignal = pyqtSignal(str)  # 发送文件路径
    n_match_pySignal = pyqtSignal(int)  # 发送匹配文本数量到search bar
    finding_state_pySignal = pyqtSignal(bool)
    key_pressed_signal = pyqtSignal(str)
    tab_pressed_signal = pyqtSignal(str)

    def __init__(self, name: str = '', *args, **kwargs):
        super(TextEdit, self).__init__(*args, **kwargs)
        self.setWrapMode(QsciScintilla.WrapNone)
        self.setObjectName(name + '_edit')
        self.setTabWidth(4)
        # self.setTabStopWidth(4)
        # self.setTabChangesFocus(False)
        self.setAcceptDrops(True)
        self.setAutoFillBackground(True)
        self.setUtf8(True)
        self._finding_state = False

        self.set_font()
        self._cursor_pre_pos = None
        self.mode = ''  # 模式
        self._diags_prefix = ''  # diags下的命令行前缀
        self.cmd_history = {'OS': deque(maxlen=100),
                            'Diags': deque(maxlen=100),
                            'Recovery': deque(maxlen=100)}  # 用来存储当前模式下的cmd历史记录
        self.prefix = {'OS': '~ root# ',
                       'Diags': self.diags_prefix,
                       'Recovery': '\] '}
        self._tracker = 0
        self._cmd_long = ''

        self.setVerticalScrollBar(ScrollBar())
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        # self.setReadOnly(True)
        # ----------- 文本光标 ------------
        self.setCaretWidth(caret_width)  # 设置光标外形宽度
        self.setCaretLineVisible(caret_line_visible)
        self.setCaretLineBackgroundColor(line_background_color)  # 光标行背景颜色
        # --------- 自动补全 ----------
        self.setAutoCompletionCaseSensitivity(False)  # auto complete
        self.setAutoCompletionThreshold(1)  # 自动补全起始字符数
        self.setAutoCompletionReplaceWord(True)  # 自动补全会替换后面的字符
        self.setAutoCompletionSource(QsciScintilla.AcsAll)  # 自动补全资源
        # --------- 行号 ----------
        font_metrics = self.fontMetrics()
        self.setMarginsFont(self.font())
        self.setMarginWidth(0, font_metrics.width('000') + 6)
        self.setMarginType(0, QsciScintilla.NumberMargin)
        # --------- 缩进 ----------
        self.setTabIndents(True)
        self.setIndentationWidth(4)
        self.setBackspaceUnindents(True)
        self.setIndentationGuides(True)
        # --------- 括号匹配 ----------
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
        # --------- 换行符 ---------
        self.setEolMode(QsciScintilla.EolMac)


        # self.installEventFilter(self)
        # self.delay = 100b
        # self.timer = QTimer(self)

        # ---------- 语法高亮 -----------
        self.lexer = Lexer(self)
        self.setLexer(self.lexer)

        self.finding_state_pySignal.connect(self.finding_state_changed)
        self.installEventFilter(self)
        self.key_pressed_signal.connect(self.append_cmd_history)

    def append_cmd_history(self, cmd: str):  # TODO not ready
        if self.mode and valid_cmd_pat.search(cmd):
            self.cmd_history.get(self.mode).append(cmd.strip())
            logger.debug(f"{self.mode} mode append cmd history: {repr(cmd)}")

    def insert_at_end(self, text: str):
        self.setCursorPosition(-1, -1)
        self.insert(text)

    def set_font(self):
        # font = QFont('Arial')
        font = QFont('Courier')
        font.setFixedPitch(True)
        font.setPointSize(12)
        self.setFont(font)
        pal = self.palette()
        pal.setColor(QPalette.Text, QColor(qRgba(0, 0, 0, 255)))
        self.setPalette(pal)

    @property
    def cursor_pre_pos(self):
        return self._cursor_pre_pos

    @cursor_pre_pos.setter
    def cursor_pre_pos(self, p):
        self._cursor_pre_pos = p

    @property
    def cmd_long(self):
        return self._cmd_long

    @cmd_long.setter
    def cmd_long(self, cmd: str):
        logger.debug(f"cmd changed: {repr(cmd)}")
        self._cmd_long = cmd

    @property
    def diags_prefix(self):
        return self._diags_prefix

    @diags_prefix.setter
    def diags_prefix(self, prefix: str):
        self._diags_prefix = prefix
        self.prefix.__setitem__('Diags', prefix)

    @property
    def finding_state(self):
        return self._finding_state

    @finding_state.setter
    def finding_state(self, v: bool):
        self._finding_state = bool(v)

    # def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
    #     if a1.type() == QEvent.KeyRelease:
    #         if a1.key() == Qt.Key_Backspace:  # backspace
    #             self.setReadOnly(False)
    #     elif a1.type() == QEvent.KeyPress:
    #         key = a1.text()
    #         if a1.key() == Qt.Key_Return:  # return
    #             key = '\n'
    #             cmd = self.cmd_long
    #             logger.debug(f"sendding cmd {repr(cmd)}")
    #             for _ in range(len(cmd)):
    #                 self.textCursor().deletePreviousChar()
    #             self.key_pressed_signal.emit(cmd + key)
    #             self.cmd_long = ''
    #             return True
    #         elif a1.key() == Qt.Key_Backspace:  # backspace
    #             prefix_len = len(self.prefix.get(self.mode, ''))
    #             if prefix_len < len(self.textCursor().block().text()):
    #                 self.del_previous_char()
    #             if self.cmd_long:
    #                 self.cmd_long = self.cmd_long[:-1]
    #             return True
    #             # cur: QTextCursor = self.textCursor()
    #             # cur.deletePreviousChar()
    #             # self.setTextCursor(cur)
    #         elif a1.key() == Qt.Key_Tab:  # tab
    #             logger.debug(f"cmd long: {self.cmd_long}")  # TODO not finished
    #             key = self.cmd_long + '\t'
    #             for _ in range(len(self.cmd_long)):
    #                 self.textCursor().deletePreviousChar()
    #             # self.key_pressed_signal.emit(key)
    #             self.tab_pressed_signal.emit(key)
    #             return True
    #         elif a1.key() == Qt.Key_Up:  # arrow up
    #             cursor = self.textCursor()
    #             cursor.select(QTextCursor.BlockUnderCursor)
    #             cursor.removeSelectedText()
    #             self.appendPlainText(self.prefix.get(self.mode))
    #             if self.cmd_history.get(self.mode):
    #                 try:
    #                     self._tracker -= 1
    #                     last_cmd = self.cmd_history.get(self.mode)[self._tracker]
    #                 except IndexError:
    #                     self._tracker = -1
    #                     last_cmd = self.cmd_history.get(self.mode)[self._tracker]
    #                 self.cmd_long = last_cmd
    #                 self.insertPlainText(last_cmd)
    #             return True
    #         elif a1.key() == Qt.Key_Down:  # arrow down
    #             cursor = self.textCursor()
    #             cursor.select(QTextCursor.BlockUnderCursor)
    #             cursor.removeSelectedText()
    #             self.appendPlainText(self.prefix.get(self.mode))
    #             if self.cmd_history.get(self.mode):
    #                 try:
    #                     self._tracker += 1
    #                     last_cmd = self.cmd_history.get(self.mode)[self._tracker]
    #                 except IndexError:
    #                     self._tracker = 0
    #                     last_cmd = self.cmd_history.get(self.mode)[self._tracker]
    #                 self.cmd_long = last_cmd
    #                 self.insertPlainText(last_cmd)
    #             return True
    #         elif a1.matches(QKeySequence.Paste):
    #             logger.debug("allocate paste")
    #             key = app.clipboard().text()
    #         elif a1.key() == Qt.Key_Control:
    #             logger.debug(f"pressed ctrl")
    #             return True
    #         elif a1.key() == Qt.Key_Meta:
    #             logger.debug(f"pressed meta")
    #
    #         elif a1.text() in string.printable:
    #             # self.key_pressed_signal.emit(key)
    #             self.cmd_long += key
    #             self._tracker = 0  # reset tracker to 0
    #             # self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
    #             self.reset_cursor_pos()
    #             # self.insertPlainText(key)
    #             self.inser(key)
    #             # self.text
    #             logger.debug(f"Pressed key {repr(key)} {a1.key()}")
    #             return True
    #     return super(TextEdit, self).eventFilter(a0, a1)

    def mousePressEvent(self, e) -> None:
        if not isinstance(self.cursor_pre_pos, int):

            # self.cursor_pre_pos = self.textCursor().position()
            self.cursor_pre_pos = self.getCursorPosition()
        super(TextEdit, self).mousePressEvent(e)

    def reset_cursor_pos(self):
        """鼠标点击会改变光标位置, 记住光标原位置，输入的时候光标回到原位置"""
        if isinstance(self.cursor_pre_pos, int):
            cur = self.textCursor()
            cur.setPosition(self.cursor_pre_pos)
            self.setTextCursor(cur)
            self.cursor_pre_pos = None

    def del_previous_char(self, n: int = 1):
        for _ in range(n):
            self.textCursor().deletePreviousChar()

    def finding_state_changed(self, v: bool):
        """slot"""
        if v:  # v pass
            if not self._finding_state:  # v
                self._finding_state = bool(v)
                self.setStyleSheet('background: rgba(200, 200, 200, 100)')
            else:
                pass
            pal = self.palette()
            pal.setColor(QPalette.Highlight, QColor(qRgba(255, 255, 62, 255)))
            self.setPalette(pal)
        else:  # v false
            if self._finding_state:
                self._finding_state = bool(v)
                self.setStyleSheet('background: #FFFFFF')

    def get_prefix(self):
        text = self.textCursor().block().text()
        try:
            prefix = mode_mark.match(text).group(1)
            return prefix
        except AttributeError as e:
            logger.error(f"{mode_mark.pattern} match {text}")
            return ""

    def find_text(self, find: dict):
        """
        1. 查找内容 2.
        :param find: {'exp': 'str or re', 'options': QTextDocument::FindFlags, 'direction': 'up or down'}
        :return:
        """
        self.setStyleSheet('background: rgba(200, 200, 200, 100)')
        exp = find.get('exp')
        options = find.get('options')
        logger.debug(f'Finding {exp} with options {options}...')
        self.count_thread = CountThread(exp, self.toPlainText())
        self.count_thread.count_signal.connect(self.set_n_matches)
        self.count_thread.start()

        r = self.find(exp, options) if options else self.find(exp)
        # if not self.highlighter.find_text:
        #     self.highlighter = Highlighter(self.document(), find_text=exp)
        previous_postion = self.textCursor().position()
        if not r:  # 没有搜索到内容
            # 如果是向下搜索，则将光标移到开始位置，否则移到最后
            self.moveCursor(QTextCursor.Start, QTextCursor.MoveAnchor) \
                if find.get('direction') == 'down' else \
                self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
            r = self.find(exp, options) if options else self.find(exp)

        if r:
            logger.debug(f"Finded {exp}")
            # pal = self.palette()
            # pal.setColor(QPalette.Highlight, QColor(qRgba(255, 255, 62, 255)))
            # self.setPalette(pal)
        else:
            self.moveCursor(previous_postion, QTextCursor.MoveAnchor)

    def text_changed_start_timer(self):
        if self.timer.isActive():
            self.timer.stop()
        self.timer.start(self.delay)

    def move_slider(self, vm: tuple):
        v, max = vm
        _max = self.verticalScrollBar().maximum()
        multiple = max / _max
        v = v / multiple
        self.verticalScrollBar().setValue(int(v))

    def dragEnterEvent(self, a0: QDragEnterEvent) -> None:
        if a0.mimeData().hasUrls():
            a0.accept()
        else:
            a0.ignore()

    def dropEvent(self, a0: QDropEvent) -> None:
        logger.debug(f'Source: {a0.source()}')
        if a0.mimeData().hasUrls():
            file_path = a0.mimeData().urls()[0].toLocalFile()
            logger.debug(file_path)
            self.open_pySignal.emit(file_path)
            a0.accept()
        else:
            a0.ignore()

    def set_n_matches(self, count: int):
        """receive count from thread and emit to search bar widget"""
        self.n_match_pySignal.emit(count)

    def search_bar_hide_event(self):
        logger.debug('Search bar hidden')
        # self.highlighter = Highlighter(self.document())
        # self.highlighter.find_text = ''

    def find_(self, exp: QRegularExpression):
        text = self.toPlainText()
        postion = self.textCursor().position()

        i = exp.globalMatch(text)
        i.next()
        exp.captureCount()

    # def mousePressEvent(self, e) -> None:
    #     logger.debug(f"printed {self.objectName()}")
    #     return super(TextEdit, self).mousePressEvent(e)
    def wheelEvent(self, e):
        ''' Ctrl + 滚轮 控制字体缩放 '''
        if QApplication.keyboardModifiers() == Qt.ControlModifier:
            da = e.angleDelta()
            if da.y() > 0:
                self.zoomIn(1)  # QsciScintilla 自带缩放的功能。参数是增加的字体点数
            elif da.y() < 0:
                self.zoomOut(1)
        else:
            super().wheelEvent(e)  # 留点汤给父类，不然滚轮无法翻页


class CountThread(QThread):
    count_signal = pyqtSignal(int)

    def __init__(self, keyword, text: str):
        super(CountThread, self).__init__()
        self.k = keyword if type(keyword) == QRegularExpression else QRegularExpression(keyword)
        self.t = text

    def run(self) -> None:
        logger.debug(f"thread running")
        gm = self.k.globalMatch(self.t)
        _count = 0
        while gm.hasNext():
            _ = gm.next()
            _count += 1

        logger.debug(f"count = {_count}, emitting")
        self.count_signal.emit(_count)


class Tracker:
    def __init__(self, *args, **kwargs):
        self._tracker = 0

    def __call__(self, direction: str = 'up'):
        if direction.lower() == 'up':
            self.tracker -= 1
        elif direction.lower() == 'down':
            self.tracker += 1
        elif direction.lower() == 'reset':
            self.tracker = 0
        return self.tracker

    @property
    def tracker(self):
        return self._tracker

    @tracker.setter
    def tracker(self, t: int):
        if t <= 0:
            self._tracker = t


class Lexer(QsciLexerCustom):
    def __init__(self, parent):
        super(Lexer, self).__init__(parent)
        self.setDefaultColor(QColor('#000000'))
        self.setDefaultPaper(QColor('#ffffff'))
        self.setDefaultFont(QFont("Courier", 12))
        self._init_color()

    def _init_color(self):
        for color, v in color_pat.items():
            self.setColor(font_style.get(color, {}).get('fgc', QColor('#000000')), v.get('level'))
            self.setPaper(font_style.get(color, {}).get('bgc', QColor('#ffffff')), v.get('level'))
            self.setFont(QFont("Courier", 12, weight=QFont.Bold), v.get('level')) if v.get('level') else ''

    def language(self):
        return "LogViewer"

    def description(self, style):
        description = "Custom lexer for the serial interactive"
        return description

    def styleText(self, start: int, end: int) -> None:
        # logger.debug(f"start: {start}, end: {end}")
        self.startStyling(start)
        text = self.parent().text()[start: end]

        pat = re.compile(r"(error:.*|warning:.*|\n|\s+|\w+|\W)")
        tokens = [(cont, len(bytearray(cont, "utf-8"))) for cont in pat.findall(text)]
        for i, (cont, len_) in enumerate(tokens):
            if cont.lower() in color_pat['green']['key']:
                self.setStyling(len_, color_pat['green']['level'])
            elif cont.lower() in color_pat['yellow']['key']:
                self.setStyling(len_, color_pat['yellow']['level'])
            elif cont.lower().startswith('error:') or cont.lower().startswith('warning:') or cont.lower() in color_pat['red']['key']:
                self.setStyling(len_, color_pat['red']['level'])
            else:
                self.setStyling(len_, color_pat['default']['level'])


if __name__ == '__main__':
    app = QApplication(sys.argv)
    t = TextEdit()
    t.show()
    sys.exit(app.exec_())
